package org.xmind.org.freehep.util;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Locale;

/**
 * This code formats numbers in Scientific Notation. The input Number object is
 * returned as a ScientificFormated string. There are two output styles: Pure
 * and Standard scientific notation. Pure formatted numbers have precisely the
 * number of digits specified by the significant digits (sigDig) parameter and
 * always specify a Base 10 Exponential(E). Standard formated numbers have the
 * number of digits specified by the significant digits (sigDig) parameter but
 * will not have a Base 10 Exponential(E) if the number of digits in the
 * mantissa <= maxWidth.
 *
 * @author Paul Spence
 * @author Mark Donszelmann
 * @author Jason Wong
 */

public class ScientificFormat extends Format {
    /**
     * The number of significant digits the number is formatted to is recorded
     * by sigDigit. The maximum width allowed for the returned String is
     * recorded by MaxWidth
     */
    private int sigDigit = 5;
    private int maxWidth = 8;
    private boolean sciNote = false; // set to true for pure Scientific Notation
    private DecimalFormat decimalFormat;
    private static final long serialVersionUID = -1182686857248711235L;

    public ScientificFormat() {

    }

    /**
     * Sets the significant digits, maximum allowable width and number
     * formatting style (SciNote == true for Pure formatting).
     */
    public ScientificFormat(int sigDigit, int maxWidth, boolean SciNote) {
        setSigDigits(sigDigit);
        setMaxWidth(maxWidth);
        setScientificNotationStyle(SciNote);
    }

    /**
     * Implementation of inherited abstract method. Checks to see if object to
     * be formatted is of type Number. If so casts the Number object to double
     * and calls the format method. Returns the result.
     */
    public StringBuffer format(Object obj, StringBuffer toAppendTo,
            FieldPosition pos) {
        if (obj instanceof Number) {
            String result = format(((Number) obj).doubleValue());
            return toAppendTo.append(result);
        } else if (obj instanceof DoubleWithError) {
            DoubleWithError dwe = (DoubleWithError) obj;
            toAppendTo.append(format(dwe.getValue()));
            if (dwe.hasAsymmetricError()) {
                toAppendTo.append(DoubleWithError.plus);
                int errorSigDigit = resolveErrorSigDigit(dwe.getValue(),
                        dwe.getPlusError());
                toAppendTo.append(format(dwe.getPlusError(), errorSigDigit));

                toAppendTo.append(DoubleWithError.minus);
                errorSigDigit = resolveErrorSigDigit(dwe.getValue(),
                        dwe.getMinError());
                toAppendTo.append(format(dwe.getMinError(), errorSigDigit));
            } else {
                toAppendTo.append(DoubleWithError.plusorminus);
                int errorSigDigit = resolveErrorSigDigit(dwe.getValue(),
                        dwe.getError());
                toAppendTo.append(format(dwe.getError(), errorSigDigit));
            }
            return toAppendTo;
        } else
            throw new IllegalArgumentException(
                    "Cannot format given Object as a Number"); //$NON-NLS-1$
    }

    /**
     * Dummy implementation of inherited abstract method.
     */
    public Object parseObject(String source, ParsePosition pos) {
        return null;
    }

    /**
     * Returns the number of significant digits
     */
    public int getSigDigits() {
        return sigDigit;
    }

    /**
     * Returns the maximum allowable width of formatted number excluding any
     * exponentials
     */
    public int getMaxWidth() {
        return maxWidth;
    }

    /**
     * Returns the formatting style: True means Pure scientific formatting,
     * False means standard.
     */
    public boolean getScientificNotationStyle() {
        return sciNote;
    }

    /**
     * Sets the number of significant digits for the formatted number
     */
    public void setSigDigits(int SigDigit) {
        if (SigDigit < 1)
            throw new IllegalArgumentException("sigDigit"); //$NON-NLS-1$
        sigDigit = SigDigit;
        decimalFormat = null;
    }

    /**
     * Sets the maximum allowable length of the formattted number mantissa
     * before exponential notation is used.
     */
    public void setMaxWidth(int mWidth) {
        if (mWidth < 3)
            throw new IllegalArgumentException("maxWidth"); //$NON-NLS-1$
        maxWidth = mWidth;
    }

    /**
     * Sets the format style used. There are two output styles: Pure and
     * Standard scientific notation. Pure formatted numbers have precisely the
     * number of digits specified by the significant digits (sigDig) parameter
     * and always specify a Base 10 Exponential(E). Standard formated numbers
     * have the number of digits specified by the significant digits (sigDig)
     * parameter but will not have a Base 10 Exponential(E) if the number of
     * digits in the mantissa <= maxWidth.
     */
    public void setScientificNotationStyle(boolean sciNote) {
        this.sciNote = sciNote;
    }

    // simplify method for taking log base 10 of x
    private final static double k = 1 / Math.log(10);

    private double Log10(double x) {
        if (x == 0)
            return 0;
        else
            return Math.log(x) * k;
    }

    private int resolveErrorSigDigit(double x, double dx) {
        // dx should never be negative
        dx = Math.abs(dx);
        // make x +ve cause negative doesn't effect sigdigits
        x = Math.abs(x);

        // these circumstances errorsigdit does equal sigdigit, excluding
        // infinity and Nan which are handled by format
        if (dx == 0 || Double.isInfinite(dx) || Double.isNaN(dx) || dx >= x)
            return sigDigit;

        // fail cases for log, method fails to handle
        if (x == 0 || Double.isInfinite(x) || Double.isNaN(x))
            return sigDigit;

        // otherwise solve for cases when dx<x
        int log = (int) Math.round(Log10(dx / x));// always will return negative
                                                  // number
        int errorsigdigit = sigDigit + log;
        if (errorsigdigit < 1)
            return 1;
        return errorsigdigit;
    }

    private DecimalFormat getDecimalFormat(int sigDig) {
        StringBuffer buffer = new StringBuffer("0."); //$NON-NLS-1$
        for (int i = 1; i < sigDig; i++)
            buffer.append('0');
        buffer.append("E0"); //$NON-NLS-1$
        return new DecimalFormat(buffer.toString(), new DecimalFormatSymbols(
                Locale.US));
    }

    /**
     * Format the number using scientific notation
     */
    public String format(double d) {
        return format(d, sigDigit);
    }

    private String format(double d, int sigDig) {
        // Deal with a few special values first
        if (Double.isInfinite(d))
            return maxWidth < 8 ? "INF" : "Infinite"; //$NON-NLS-1$ //$NON-NLS-2$
        if (Double.isNaN(d))
            return "NaN"; //$NON-NLS-1$

        // Delegate the hard part to decimalFormat
        if (decimalFormat == null)
            decimalFormat = getDecimalFormat(sigDigit);
        DecimalFormat format = (sigDig == sigDigit) ? decimalFormat
                : getDecimalFormat(sigDig);

        String preliminaryResult = format.format(d);
        if (sciNote)
            return preliminaryResult;

        int ePos = preliminaryResult.indexOf('E');
        int exponent = Integer.parseInt(preliminaryResult.substring(ePos + 1)) + 1;
        if (exponent > maxWidth)
            return preliminaryResult;
        if (exponent < -maxWidth + sigDig + 1)
            return preliminaryResult;

        // We need to fix up the result

        int sign = preliminaryResult.charAt(0) == '-' ? 1 : 0;
        StringBuffer result = new StringBuffer(preliminaryResult.substring(
                sign, sign + 1) + preliminaryResult.substring(sign + 2, ePos));

        if (exponent >= sigDig) {
            for (int i = sigDig; i < exponent; i++)
                result.append('0');
        } else if (exponent < 0) {
            result.insert(0, "."); //$NON-NLS-1$
            for (int i = exponent; i < 0; i++)
                result.insert(1, '0');
        } else {
            result.insert(exponent, '.');
        }
        if (sign > 0)
            result.insert(0, '-');
        return result.toString();
    }
    // /**
    // * Format a number plus error using scientific notation
    // */
    // public String formatError(double d,double dx)
    // {
    // return format(dx, resolveErrorSigDigit(d, dx));
    // }

}